package helpmeigor;

import java.util.logging.Logger;
import com.google.wave.api.*;

import java.util.regex.Pattern;
import java.util.regex.Matcher;

import java.util.List;

import javax.jdo.PersistenceManager;

public class HelpmeigorServlet extends AbstractRobotServlet {
	private static final long serialVersionUID = 6456714286746166045L;
	private static final Logger logging = Logger.getLogger(HelpmeigorServlet.class.getName());
	private Pattern commandPattern = Pattern.compile("\\(cite (.*?)\\)",  Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
	
	@Override
	public void processEvents(RobotMessageBundle bundle) {
		Wavelet wavelet = bundle.getWavelet();
				
		if (bundle.wasSelfAdded()) {
			Blip blip = wavelet.appendBlip();
			TextView textView = blip.getDocument();
			textView.append("Hi, I'm Igor. I can help you write scientific papers - see http://blogs.nature.com/wp/nascent/2009/07/igor_a_google_wave_robot_to_ma.html for more details.");
			String timeString = String.valueOf(System.currentTimeMillis());
			textView.setAnnotation("igor_welcome_message", timeString);
			logging.info("Added Igor to a new wave " + blip.getWavelet().getWaveId());
		}  
    
		for (Event e: bundle.getEvents()) {
			if (e.getType() == EventType.BLIP_SUBMITTED) {
				Blip changedBlip = e.getBlip();				
				processChangedBlip(changedBlip);
				logging.info("Noticed a blip changed " + changedBlip.getBlipId());
			}
		}
	}
	
	// sent blips that have been modified. Searches them for commands to the robot
	// and issues blip operations accordingly.
	private void processChangedBlip(Blip blip) {
		// instantiate a reference helper class.
		ReferenceHelper refHelper = new ReferenceHelper();
		
		deleteOldStatusMessages(blip);
		
		// make sure that the current set of references are in order, none are missing etc.
		boolean changedReferences = refHelper.checkExistingReferences(blip);

		// parse any commands that the user has sent to Igor in the text.
		boolean parsedCommands = parseCommands(blip, false, "");
		if (parsedCommands) {
			// have to check references are in order again now as the user might have just inserted one
			// at the start.
			refHelper.checkExistingReferences(blip);
		}

		// just for debugging pretend we parsed some commands and need to rerender things...
		// parsedCommands = true;
		
		// redo the references block if we need to.
		if (changedReferences || parsedCommands) {
			refHelper.rebuildReferences(blip);
		}
	}
	
	// deletes status messages left in the text. These are attached to annotations.
	private boolean deleteOldStatusMessages(Blip blip) {
		TextView document = blip.getDocument();
		
		// todo: need to generalize this to handle any type of status message...
		// delete any annotation references that aren't mentioned in the text any more.
		List<Annotation> annotations = document.getAnnotations("igor_status_message_sorry");
		for (int i=0; i < annotations.size(); i++) {
			Annotation temp = annotations.get(i);
			document.delete(temp.getRange());
		}
		document.deleteAnnotations("igor_status_message_sorry");
		
		return true;
	}
	
	private void addStatusMessage(Blip blip, int position, String message, String value) {
		TextView textView = blip.getDocument();
		textView.insert(position, message);		
		// highlight the message.
		textView.setAnnotation(new Range(position, position + message.length()), "style/backgroundColor", "rgb(255, 69, 0)");
		textView.setAnnotation(new Range(position, position + message.length()), "style/color", "rgb(255, 255, 255)");
		textView.setAnnotation(new Range(position, position + message.length()), "igor_status_message_sorry", value);

	}
	
	// scans the text of a blip for commands, acts accordingly. Called recursively as each command is found
	// so that character positions of searched and eventual output texts are kept in sync.
	private boolean parseCommands(Blip blip, boolean parsedCommands, String citesToIgnore) {
		ReferenceHelper refHelper = new ReferenceHelper();
		String useUsername = "";
		String useService = "pubmed";
		
		TextView textView = blip.getDocument();
		String textViewString = textView.getText();
		Matcher commandMatcher = commandPattern.matcher(textViewString);
		int referencesStartAt = refHelper.getReferencesBlockStart(blip);
		while (commandMatcher.find()) {
			// continue if we've found something on the citesToIgnore list.
			if (citesToIgnore.contains(commandMatcher.group(0))) {
				logging.info("Just matched " + commandMatcher.group(0) + " but it's on the ignore list so moving on.");
				continue;
			}
			
			// if we're now looking at the References: section then end here.
			if ((referencesStartAt >= 0) && (commandMatcher.start() > referencesStartAt)) {return parsedCommands;}
			
			// is it a 'from' command?
			if (commandMatcher.group(1).startsWith("from")) {
				// parse this command, remove it then continue.
				if (commandMatcher.group(1).startsWith("from pubmed")) {
					// that was easy - use PubMed.
					useUsername = "";
					useService = "pubmed";
					textView.delete(new Range(commandMatcher.start(), commandMatcher.end())); 
				} else {
					// nope, we're going to have to parse out the username and service.
					String[] parts = commandMatcher.group(1).split(" ");
					if (parts.length == 3) {
						if ((parts[1].compareToIgnoreCase("citeulike") == 0) || (parts[1].compareToIgnoreCase("connotea") == 0)) {
							useService = parts[1];
							useUsername = parts[2];
							textView.delete(new Range(commandMatcher.start(), commandMatcher.end())); 
							// not sure if we should tell the user which library we're searching...
							// on the one hand it'd be good to keep track. On the other it stops us from
							// switching libraries mid-text.
						} else {
							String message = " Igor says: sorry, right now the only services supported are connotea, pubmed and citeulike ";
							addStatusMessage(blip, commandMatcher.end(), message, commandMatcher.group(0));						
							citesToIgnore = citesToIgnore + commandMatcher.group(0);
							return parseCommands(blip, true, citesToIgnore);
						}
					} else {
						String message = " Igor says: the correct usage is cite from <service> [<username>] e.g. cite from connotea euanadie ";
						addStatusMessage(blip, commandMatcher.end(), message, commandMatcher.group(0));						
						citesToIgnore = citesToIgnore + commandMatcher.group(0);
						return parseCommands(blip, true, citesToIgnore);
					}
				}		
				
				// update datastore with username and service to use on this Wave.
				PersistenceManager pm = PMF.get().getPersistenceManager();
				RefmanDetails details = new RefmanDetails(blip.getWavelet().getWaveId(), useUsername, useService);
				logging.info("Should have created entity with username " + details.getUsername());
				try {
					pm.makePersistent(details);
		        } finally {
		        	pm.close();
		        }
				
				return parseCommands(blip, true, citesToIgnore);
			}
			
			// determine which username and service to use by checking the datastore with this wave's id.
			PersistenceManager pm = PMF.get().getPersistenceManager();
			try {
				RefmanDetails e = pm.getObjectById(RefmanDetails.class, blip.getWavelet().getWaveId());
				// if that worked overwrite the defaults.
				if (!e.getService().isEmpty()) {
					useService = e.getService();
					useUsername = e.getUsername();
				}
			} catch (javax.jdo.JDOObjectNotFoundException e) {
				logging.severe("Haven't created a data record for this wave yet.");
			} finally {
				pm.close();
			}
			
			String fullCitation = "";
			if (commandMatcher.group(1).startsWith("http://")) {
				// we're citing a website.
				fullCitation = commandMatcher.group(1);
			} else {
				// otherwise go via the proxy.
				fullCitation = refHelper.callLookupProxy(commandMatcher.group(1), useUsername, useService);
			}
			
			if (fullCitation.startsWith("Sorry,")) {
				// we're not dealing with a simple lookup. Could be a few different things:
				// a) multiple matches
				// b) server fail
				// c) no matches
				// how do we avoid being put into an infinite loop by this one?
				citesToIgnore = citesToIgnore + commandMatcher.group(0);
				logging.severe("Got a sorry back from server for " + commandMatcher.group(1)+ " " + fullCitation);
				
				// tell the user about this problem. An inline blip would be perfect but sadly there are
				// too many problems rendering and keeping track of them.				
				String message = " Igor says: " + fullCitation + " ";
				addStatusMessage(blip, commandMatcher.end(), message, commandMatcher.group(0));
				return parseCommands(blip, true, citesToIgnore);
			}
			
			// what reference number will we give this citation if it's new?
			int referenceNumber = refHelper.countReferences(blip) + 1;
			
			// check to see if we're already using fullCitation in this document - if so, use the existing ref number.
			Integer existingReference = refHelper.haveAlreadyCited(blip, fullCitation);
			if (existingReference >= 0) {
				referenceNumber = existingReference.intValue();
			}		
			
			// otherwise create a new reference number and associated annotation.
			String referenceNumberString = String.valueOf(referenceNumber);
			
			// I think that .replace() doesn't work particularly well with differently sized strings,
			// so trying something else for now.
			//textView.replace(new Range(commandMatcher.start(), commandMatcher.end()), "[" + referenceNumberString +  "]");
			textView.delete(new Range(commandMatcher.start(), commandMatcher.end())); // assuming this changes the positions across the whole document.
			textView.insert(commandMatcher.start(), "[" + referenceNumberString +  "]");
			
			
			// only add annotation if it doesn't already exist.
			if (existingReference < 0) {
				textView.setAnnotation("igor_reference_" + referenceNumberString, fullCitation);
			}
			
			logging.severe("Added new reference with key " + referenceNumberString);
			
			return parseCommands(blip, true, citesToIgnore);
		}
		return parsedCommands;
	}
}

